/*
 * This file is part of OpenModelica.
 *
 * Copyright (c) 1998-CurrentYear, Open Source Modelica Consortium (OSMC),
 * c/o Linköpings universitet, Department of Computer and Information Science,
 * SE-58183 Linköping, Sweden.
 *
 * All rights reserved.
 *
 * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF GPL VERSION 3 LICENSE OR
 * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.2.
 * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE
 * OF THE OSMC PUBLIC LICENSE OR THE GPL VERSION 3, ACCORDING TO RECIPIENTS CHOICE.
 *
 * The OpenModelica software and the Open Source Modelica
 * Consortium (OSMC) Public License (OSMC-PL) are obtained
 * from OSMC, either from the above address,
 * from the URLs: http://www.ida.liu.se/projects/OpenModelica or
 * http://www.openmodelica.org, and in the OpenModelica distribution.
 * GNU version 3 is obtained from: http://www.gnu.org/copyleft/gpl.html.
 *
 * This program is distributed WITHOUT ANY WARRANTY; without
 * even the implied warranty of  MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH
 * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL.
 *
 * See the full OSMC Public License conditions for more details.
 *
 */

#include "fmu_read_flags.h"
#include "../simulation/options.h"
#include "../util/simulation_options.h"
#include "../simulation/solver/solver_main.h"
#ifdef WITH_SUNDIALS
#include "../simulation/solver/cvode_solver.h"
#endif
#include "../util/omc_mmap.h"
#include "../util/omc_file.h"

/**
  * @brief Skips to a given character or end of string.
 *
  * @param str            Null terminated string.
  * @param c              Character to move pointer to.
  * @return const char*   Position of character `c` or `\0`.
 */
static inline const char* skipTo(const char *str, char c)
{
  while(*str != c && *str != '\0') {
    str++;
  }
  return str;
}

/**
  * @brief Puts double quotes around a string
 *
  * @param str      Null terminated string.
  * @return char*   Newly allocated string with double quotes around `str`.
  *                 Needs to be freed with `free`
 */
static inline char* quote(const char *str)
{
  size_t len = strlen(str) + 3; // +2 for quotes, +1 for null terminator
  char* tmp = (char*) malloc(len);
  if (tmp == NULL) {
    return NULL;
  }
  snprintf(tmp, len, "\"%s\"", str);
  return tmp;
 }

/**
 * @brief Parse and sets the solver method string
 *
 * @param  solverInfo   Solver info to set solver method in.
 * @param  str          string that starts at solver method string
 * @return const char*  input string skipped to endline
 */
static inline const char* setSolverMethod(SOLVER_INFO *solverInfo, const char *str)
{
  /* Variables */
  int i;
  char* value;
  for(i=1; i<S_MAX; i++)
  {
    value = quote(SOLVER_METHOD_NAME[i]);
    if (value == NULL) {
      continue;
    }
    if (strncmp(str, value, strlen(SOLVER_METHOD_NAME[i]) + 2) == 0)
    {
      solverInfo->solverMethod = i;
      free(value);
      break;
    }
    free(value);
  }
  return skipTo(str, '\n');
}

/**
 * @brief parses flags from resources/modelName_flags.json
 *
 * @param solverInfo
 * @param str           string read from file
 */
void parseFlags(SOLVER_INFO *solverInfo, const char *str)
{
  /* Variables */
  int i, k;
  const int k_max = 1000;
  char* value;

  str = skipTo(str, '\"');
  k = 0;
  while(*str != '\0' && k < k_max)
  {
    k++;
    for(i=1; i<FMU_FLAG_MAX; i++)
    {
      // map the fmu flags to regular flags
      value = quote(FLAG_NAME[FMU_FLAG_MAP[i]]);
      if (value == NULL) {
        /* If allocation failed, skip this entry */
        continue;
      }

      if (strncmp(str, value, strlen(FLAG_NAME[FMU_FLAG_MAP[i]]) + 2) == 0)
      {
        str = skipTo(str, ':');
        str = skipTo(str, '\"');

        switch(i) {
          case FMU_FLAG_SOLVER: str = setSolverMethod(solverInfo, str); break;
          default: str = skipTo(str, '\n'); break;
        }
      }

      free(value);
    }
    str = skipTo(str, '\"');
   }
}

/**
 * @brief Initialize solver data.
 *
 * Reads optional FMU simulation flags from <fmiPrefix>.fmu/resources/<fmiPrefix>_flags.json.
 * Initialize solver euler or CVODE.
 *
 * @param comp          FMU component.
 * @return int          Return 0 on success and -1 when an error occurred.
 */
int FMI2CS_initializeSolverData(ModelInstance* comp)
{
  /* Variables */
  const fmi2CallbackFunctions* functions;
  DATA* data;
  threadData_t* threadData;
  SOLVER_INFO* solverInfo;

  int retValue;

  functions = comp->functions;
  data = comp->fmuData;
  threadData = comp->threadData;

  /* Allocate memory */
  solverInfo = (SOLVER_INFO*) functions->allocateMemory(1, sizeof(SOLVER_INFO));

  /* Initialize solverInfo */
  solverInfo->currentTime = 0;
  solverInfo->currentStepSize = 0;
  solverInfo->laststep = 0;
  solverInfo->solverMethod = 0;
  solverInfo->solverRootFinding = 0;
  solverInfo->solverNoEquidistantGrid = FALSE;
  solverInfo->lastdesiredStep = solverInfo->currentTime + solverInfo->currentStepSize;
  solverInfo->eventLst = NULL;
  solverInfo->didEventStep = 0;
  solverInfo->stateEvents = 0;
  solverInfo->sampleEvents = 0;

  /* read fmu flags from flags.json */
  size_t filename_len = strlen(comp->fmuData->modelData->resourcesDir) + strlen(comp->fmuData->modelData->modelFilePrefix) + 13;
  char* flags_filename = functions->allocateMemory(filename_len, sizeof(char));
  snprintf(flags_filename, filename_len, "%s/%s_flags.json",
           comp->fmuData->modelData->resourcesDir,
           comp->fmuData->modelData->modelFilePrefix);
  FILTERED_LOG(comp, fmi2OK, LOG_ALL, "fmi2Instantiate: Trying to find simulation settings %s.", flags_filename)

  if( omc_file_exists( flags_filename) )
  {
    FILTERED_LOG(comp, fmi2OK, LOG_ALL, "fmi2Instantiate: Found simulation settings %s.", flags_filename)
    omc_mmap_read mmap_reader = {0};
    mmap_reader = omc_mmap_open_read(flags_filename);
    parseFlags(solverInfo, mmap_reader.data);
    omc_mmap_close_read(mmap_reader);
  }
  else
  {
    FILTERED_LOG(comp, fmi2OK, LOG_ALL, "fmi2Instantiate: Using default simulation settings.")
    solverInfo->solverMethod = S_EULER;
  }

  /* If no states are present, we can use Euler's method since it is doing nothing. */
  if (data->modelData->nStates < 1)
  {
    FILTERED_LOG(comp, fmi2OK, LOG_ALL, "fmi2Instantiate: No states present, continuing without ODE solver.")
    solverInfo->solverMethod = S_EULER;
  }

  switch (solverInfo->solverMethod)
  {
    case S_EULER:
      /* Needs no initialization */
      retValue = 0;
      break;
      case S_CVODE:
#ifdef WITH_SUNDIALS
      omc_useStream[OMC_LOG_SOLVER] = 1;
      CVODE_SOLVER* cvodeData = NULL;
      FILTERED_LOG(comp, fmi2OK, LOG_ALL, "Initializing CVODE ODE Solver")
      cvodeData = (CVODE_SOLVER*) functions->allocateMemory(1, sizeof(CVODE_SOLVER));
      if (!cvodeData) {
        FILTERED_LOG(comp, fmi2Fatal, LOG_STATUSFATAL, "fmi2Instantiate: Out of memory.")
        functions->freeMemory(solverInfo);
        return -1;
        retValue = -1;
      } else {
        retValue = cvode_solver_initial(data, threadData, solverInfo, cvodeData, 1 /* is FMI */);   /* TODO: cvode_solver_initial needs to use malloc and free from fmi2CallbackFunctions */
      }
      solverInfo->solverData = cvodeData;
      omc_useStream[OMC_LOG_SOLVER] = 0;
#else
      solverInfo->solverData = NULL;
      FILTERED_LOG(comp, fmi2Fatal, LOG_STATUSFATAL, "fmi2Instantiate: FMU not compiled with SUNDIALS but solver CVODE selected.")
      retValue = -1;
#endif /* WITH_SUNDIALS */
      break;
    default:
      FILTERED_LOG(comp, fmi2Fatal, LOG_STATUSFATAL, "fmi2Instantiate: Unknown solver method.")
      retValue = -1;
  }

  functions->freeMemory(flags_filename);

  comp->solverInfo = solverInfo;

  return retValue;
}

/**
 * @brief Deinitialize solver data.
 *
 * Use for solver data allocated with FMI2CS_initializeSolverData.
 * Frees everything inside comp->solverInfo.
 *
 * @param comp          FMU component.
 * @return int          Return 0 on success and -1 else.
 */
int FMI2CS_deInitializeSolverData(ModelInstance* comp)
{
  /* Variables */
  DATA* data;
  threadData_t* threadData;
  SOLVER_INFO* solverInfo;
  const fmi2CallbackFunctions* functions;
  int retValue;

  functions = comp->functions;
  data = comp->fmuData;
  threadData = comp->threadData;
  solverInfo = comp->solverInfo;

  /* Log function call */
  FILTERED_LOG(comp, fmi2OK, LOG_ALL, "fmi2FreeInstance: Freeing solver data.")

  switch (solverInfo->solverMethod)
  {
    case S_EULER:
      /* Needs no freeing */
      retValue = 0;
      break;
    case S_CVODE:
#ifdef WITH_SUNDIALS
      retValue = cvode_solver_deinitial(solverInfo->solverData);
      break;
#else
      FILTERED_LOG(comp, fmi2Fatal, LOG_STATUSFATAL, "fmi2Instantiate: FMU not compiled with SUNDIALS but solver CVODE selected.")
      retValue = -1;
      break;
#endif /* WITH_SUNDIALS */
    default:
      FILTERED_LOG(comp, fmi2Fatal, LOG_STATUSFATAL, "fmi2FreeInstance: Unknown solver method.")
      retValue = -1;
  }

  comp->functions->freeMemory(comp->solverInfo);
  comp->solverInfo = NULL;

  return retValue;
}
